feat: enhance Jinja2 inline syntax highlighting#2715
feat: enhance Jinja2 inline syntax highlighting#2715Neo2SHYAlien wants to merge 2 commits intoansible:mainfrom
Conversation
- Add statement ({% %}) and comment ({# #}) support with trim variants
- Add missing keywords: break, continue, call/endcall, endfilter, with/endwith, extends, from, do, raw/endraw
- Add ~ (concatenation), numeric literals, and comparison operator fixes
- Support `is not` test syntax, variable assignment, and function calls
- Separate logical operators (and, or, not, in) from control keywords
- Add numeric dot-access (item.0) and whitespace tolerance after | and .
- Fix >= operator (was =>), scope naming consistency (.jinja suffix)
- Remove duplicate literal patterns from jinja-extras injection
- Add `until` to conditional keywords, clean up scope names
📝 WalkthroughWalkthroughExpanded and refined Jinja highlighting across Ansible grammars: added statement and comment injections, generalized delimiters with optional hyphens, renamed punctuation scopes, added Changes
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Poem
🚥 Pre-merge checks | ✅ 3✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 3
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@syntaxes/ansible/jinja-braces.tmLanguage.plist`:
- Around line 6-8: Add a dedicated raw-block rule before the generic
'#jinja-statement' include so `{% raw %}` / `{%- raw -%}` (and matching `{%
endraw %}` / `{%- endraw -%}`) are treated as a raw region; implement a rule
(e.g. '#jinja-raw') that uses a begin pattern matching `{%[-\s]*raw[-\s]*%}`
allowing optional whitespace-control dashes and a corresponding end pattern
matching `{%[-\s]*endraw[-\s]*%}`, mark the content as raw (no jinja-expression
highlighting) and place this rule above the '#jinja-statement' include in the
list so inner `{{ ... }}` are not re-scanned; apply the same change to the other
occurrences mentioned (lines 29-46) to ensure consistent behavior.
In `@syntaxes/external/jinja.tmLanguage.json`:
- Line 379: The file ends with the final closing brace '}' but is missing a
trailing newline, causing the end-of-file-fixer CI error; add a single newline
character after the final '}' in jinja.tmLanguage.json so the file terminates
with a newline (restore the trailing newline at EOF).
- Around line 153-160: The current regex in the "match" property that assigns
entity.name.function.jinja uses a negative lookahead (?!and|or|not|in\b) which
also blocks identifiers starting with those sequences; update that negative
lookahead to require word boundaries around the keywords so only whole keywords
are excluded (i.e., change the lookahead to assert complete keyword boundaries)
while leaving the rest of the pattern ([a-zA-Z_][a-zA-Z0-9_]*\s*(?=\())))
unchanged so calls like order(...), notify(...), android(...) will be correctly
captured as functions.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 2f9ba6cd-04e6-4bb2-b946-b6a842260f65
⛔ Files ignored due to path filters (6)
syntaxes/ansible/generated/jinja-braces.tmLanguageis excluded by!**/generated/**syntaxes/ansible/generated/jinja-conditionals.tmLanguageis excluded by!**/generated/**syntaxes/ansible/generated/jinja-double-quote-escape.tmLanguageis excluded by!**/generated/**syntaxes/ansible/generated/jinja-extras.tmLanguageis excluded by!**/generated/**syntaxes/ansible/generated/jinja-single-quote-escape.tmLanguageis excluded by!**/generated/**syntaxes/ansible/generated/keywords.tmLanguageis excluded by!**/generated/**
📒 Files selected for processing (4)
syntaxes/ansible/jinja-braces.tmLanguage.plistsyntaxes/ansible/jinja-conditionals.tmLanguage.plistsyntaxes/ansible/jinja-extras.tmLanguage.plistsyntaxes/external/jinja.tmLanguage.json
| { include = '#jinja-expression'; }, | ||
| { include = '#jinja-statement'; }, | ||
| { include = '#jinja-comment'; } |
There was a problem hiding this comment.
🧩 Analysis chain
🌐 Web query:
Does Jinja support raw blocks with whitespace-control delimiters like {%- raw -%}and{%- endraw -%}?
💡 Result:
Yes, Jinja supports raw blocks with whitespace-control delimiters like {%- raw -%} and {%- endraw -%}. The official Jinja documentation explicitly states this feature: "Note Minus sign at the end of {% raw -%} tag cleans all the spaces and newlines preceding the first character of your raw block." This applies to both the opening and closing tags, allowing full whitespace stripping (leading with - at start, trailing with - at end).
Citations:
- 1: https://jinja.palletsprojects.com/en/latest/templates/
- 2: https://jinja.palletsprojects.com/en/stable/templates/
Add a dedicated raw-block matcher before the generic statement rule.
{% raw %} / {%- raw -%} blocks are currently not matched as a dedicated rule and fall through to #jinja-statement, so the surrounding string continues to be scanned and any inner {{ ... }} still matches #jinja-expression. Jinja supports whitespace-control delimiters on raw blocks (as confirmed by official documentation), so the grammar should prevent content inside raw/endraw from being highlighted as expressions.
Suggested fix
patterns = (
+ { include = '#jinja-raw-block'; },
{ include = '#jinja-expression'; },
{ include = '#jinja-statement'; },
{ include = '#jinja-comment'; }
);
...
+ jinja-raw-block = {
+ begin = '\{%-?\s*raw\s*-?%\}';
+ end = '\{%-?\s*endraw\s*-?%\}';
+ name = 'comment.block.jinja.raw';
+ };
jinja-statement = {Also applies to: 29-46
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@syntaxes/ansible/jinja-braces.tmLanguage.plist` around lines 6 - 8, Add a
dedicated raw-block rule before the generic '#jinja-statement' include so `{%
raw %}` / `{%- raw -%}` (and matching `{% endraw %}` / `{%- endraw -%}`) are
treated as a raw region; implement a rule (e.g. '#jinja-raw') that uses a begin
pattern matching `{%[-\s]*raw[-\s]*%}` allowing optional whitespace-control
dashes and a corresponding end pattern matching `{%[-\s]*endraw[-\s]*%}`, mark
the content as raw (no jinja-expression highlighting) and place this rule above
the '#jinja-statement' include in the list so inner `{{ ... }}` are not
re-scanned; apply the same change to the other occurrences mentioned (lines
29-46) to ensure consistent behavior.
| { | ||
| "captures": { | ||
| "1": { | ||
| "name": "entity.name.function.jinja" | ||
| } | ||
| }, | ||
| "match": "\\b(?!and|or|not|in\\b)([a-zA-Z_][a-zA-Z0-9_]*)\\s*(?=\\()" | ||
| }, |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
python - <<'PY'
import re
pattern = re.compile(r"\b(?!and|or|not|in\b)([a-zA-Z_][a-zA-Z0-9_]*)\s*(?=\()")
samples = ["order(", "notify(", "android(", "input(", "foo(", "and(", "or(", "not(", "in("]
for sample in samples:
match = pattern.search(sample)
print(f"{sample:10} -> {match.group(1) if match else None}")
PYRepository: ansible/vscode-ansible
Length of output: 236
🏁 Script executed:
python - <<'PY'
import re
# Current pattern (from the code)
current = re.compile(r"\b(?!and|or|not|in\b)([a-zA-Z_][a-zA-Z0-9_]*)\s*(?=\()")
# Suggested fix pattern
suggested = re.compile(r"\b(?!(?:and|or|not|in)\b)([a-zA-Z_][a-zA-Z0-9_]*)\s*(?=\()")
samples = ["order(", "notify(", "android(", "input(", "foo(", "and(", "or(", "not(", "in("]
print("Current pattern:")
for sample in samples:
match = current.search(sample)
print(f" {sample:10} -> {match.group(1) if match else None}")
print("\nSuggested fix pattern:")
for sample in samples:
match = suggested.search(sample)
print(f" {sample:10} -> {match.group(1) if match else None}")
PYRepository: ansible/vscode-ansible
Length of output: 490
Tighten the negative lookahead on function names.
(?!and|or|not|in\b) also rejects identifiers that only start with and, or, or not, so calls like order(...), notify(...), or android(...) never get entity.name.function.jinja. The fix is to add a word boundary inside the lookahead to match only complete keywords.
Suggested fix
- "match": "\\b(?!and|or|not|in\\b)([a-zA-Z_][a-zA-Z0-9_]*)\\s*(?=\\()"
+ "match": "\\b(?!(?:and|or|not|in)\\b)([a-zA-Z_][a-zA-Z0-9_]*)\\s*(?=\\()"📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| { | |
| "captures": { | |
| "1": { | |
| "name": "entity.name.function.jinja" | |
| } | |
| }, | |
| "match": "\\b(?!and|or|not|in\\b)([a-zA-Z_][a-zA-Z0-9_]*)\\s*(?=\\()" | |
| }, | |
| { | |
| "captures": { | |
| "1": { | |
| "name": "entity.name.function.jinja" | |
| } | |
| }, | |
| "match": "\\b(?!(?:and|or|not|in)\\b)([a-zA-Z_][a-zA-Z0-9_]*)\\s*(?=\\()" | |
| }, |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@syntaxes/external/jinja.tmLanguage.json` around lines 153 - 160, The current
regex in the "match" property that assigns entity.name.function.jinja uses a
negative lookahead (?!and|or|not|in\b) which also blocks identifiers starting
with those sequences; update that negative lookahead to require word boundaries
around the keywords so only whole keywords are excluded (i.e., change the
lookahead to assert complete keyword boundaries) while leaving the rest of the
pattern ([a-zA-Z_][a-zA-Z0-9_]*\s*(?=\()))) unchanged so calls like order(...),
notify(...), android(...) will be correctly captured as functions.
There was a problem hiding this comment.
Actionable comments posted: 1
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
syntaxes/external/jinja.tmLanguage.json (1)
109-118:⚠️ Potential issue | 🟡 MinorAdd dotted identifier support to the
filterkeyword andistest matchers.Jinja2 allows dots in filter and test identifiers for hierarchical organization (e.g.,
ansible.utils.ipv4). The inline filter matcher at line 198 already supports this pattern, but thefilterstatement (line 118) andistest (line 132) matchers only accept simple identifiers, causing dotted names to be split and fall back to property scopes for the remainder.Suggested fix
- "match": "\\s*\\b(filter)\\s+([a-zA-Z_][a-zA-Z0-9_]*)\\b" + "match": "\\s*\\b(filter)\\s+([a-zA-Z_][a-zA-Z0-9_]*(?:\\.[a-zA-Z_][a-zA-Z0-9_]*)*)\\b"- "match": "\\b(is)\\s+(not\\s+)?([a-zA-Z_][a-zA-Z0-9_]*)\\b" + "match": "\\b(is)\\s+(not\\s+)?([a-zA-Z_][a-zA-Z0-9_]*(?:\\.[a-zA-Z_][a-zA-Z0-9_]*)*)\\b"🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@syntaxes/external/jinja.tmLanguage.json` around lines 109 - 118, Update the regex for the "filter" keyword matcher and the "is" test matcher so identifiers can include dotted components (e.g., ansible.utils.ipv4); replace the current simple identifier pattern ([a-zA-Z_][a-zA-Z0-9_]*) with a dotted-identifier pattern like [a-zA-Z_][a-zA-Z0-9_]*(?:\.[a-zA-Z_][a-zA-Z0-9_]*)* in the "match" strings for the filter matcher (the block with "keyword.control.jinja" / "entity.name.function.filter.jinja") and the corresponding "is" test matcher so multi-part names are captured as a single token rather than split across scopes.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@syntaxes/external/jinja.tmLanguage.json`:
- Around line 143-152: Add a Jinja "set"‑specific target matcher to cover
multi-target and block assignments: create a pattern used when parsing the "set"
tag that captures target names as "variable.parameter.jinja" when they are
followed by an equals sign, a comma, or the end of the statement (e.g. match
names with lookahead for "\\s*(?:=|,|\\s*%\\})"), and mark the "=" with
"keyword.operator.assignment.jinja" (replace or supplement the existing generic
assignment pattern "\\b([a-zA-Z_][a-zA-Z0-9_]*)\\s*(=)(?!=)" and its captures)
so that both "{% set key, value = %}" and "{% set body %}...{% endset %}"
targets are highlighted.
---
Outside diff comments:
In `@syntaxes/external/jinja.tmLanguage.json`:
- Around line 109-118: Update the regex for the "filter" keyword matcher and the
"is" test matcher so identifiers can include dotted components (e.g.,
ansible.utils.ipv4); replace the current simple identifier pattern
([a-zA-Z_][a-zA-Z0-9_]*) with a dotted-identifier pattern like
[a-zA-Z_][a-zA-Z0-9_]*(?:\.[a-zA-Z_][a-zA-Z0-9_]*)* in the "match" strings for
the filter matcher (the block with "keyword.control.jinja" /
"entity.name.function.filter.jinja") and the corresponding "is" test matcher so
multi-part names are captured as a single token rather than split across scopes.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 382c5b23-26a9-4c37-a76b-d2027238a022
⛔ Files ignored due to path filters (3)
syntaxes/ansible/generated/jinja-braces.tmLanguageis excluded by!**/generated/**syntaxes/ansible/generated/jinja-conditionals.tmLanguageis excluded by!**/generated/**syntaxes/ansible/generated/jinja-extras.tmLanguageis excluded by!**/generated/**
📒 Files selected for processing (1)
syntaxes/external/jinja.tmLanguage.json
| "captures": { | ||
| "1": { | ||
| "name": "variable.parameter.jinja" | ||
| }, | ||
| "2": { | ||
| "name": "keyword.operator.assignment.jinja" | ||
| } | ||
| }, | ||
| "match": "\\b([a-zA-Z_][a-zA-Z0-9_]*)\\s*(=)(?!=)" | ||
| }, |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
# First, check the file exists and read the lines in question
wc -l syntaxes/external/jinja.tmLanguage.jsonRepository: ansible/vscode-ansible
Length of output: 109
🏁 Script executed:
# Read the context around lines 143-152 to see the current rule
sed -n '140,160p' syntaxes/external/jinja.tmLanguage.jsonRepository: ansible/vscode-ansible
Length of output: 902
🏁 Script executed:
# Search for all "set" related rules in the file to see the full context
rg -n "set" syntaxes/external/jinja.tmLanguage.json -A 2 -B 2Repository: ansible/vscode-ansible
Length of output: 439
🏁 Script executed:
# Read more context around the set rule - let's see a larger section
sed -n '130,160p' syntaxes/external/jinja.tmLanguage.jsonRepository: ansible/vscode-ansible
Length of output: 1242
🏁 Script executed:
# Let's see the full structure of one of these rules to understand better
sed -n '130,180p' syntaxes/external/jinja.tmLanguage.jsonRepository: ansible/vscode-ansible
Length of output: 2268
🌐 Web query:
Jinja2 set syntax multiple targets block assignments documentation
💡 Result:
Jinja2 supports multiple targets in the {% set %} tag for simultaneous variable assignments, similar to Python tuple unpacking. This works for regular variables and, since Jinja 3.2, also for namespace object attributes. Syntax examples from official documentation: Single variable: {% set navigation = [('index.html', 'Index'), ('about.html', 'About')] %} Multiple targets: {% set key, value = call_something %} For namespace objects (added in 2.10, multi-assignment in 3.2): {% set ns = namespace %} {% set ns.a, ns.b = 1, 2 %} Block assignments use {% set var %}...{% endset %} to capture block content into a single variable (added in 2.8, filters in 2.10). Multiple targets are not supported in block assignments; they assign to one variable only. Note: Variables set inside blocks/loops do not propagate outside due to scoping rules. Use namespaces for mutable state across scopes. Official documentation confirms this syntax is available in Jinja 3.1.x and 3.2.x (latest stable as of 2026).
Citations:
- 1: https://jinja.palletsprojects.com/en/latest/templates/
- 2: https://jinja.palletsprojects.com/en/stable/templates/
set targets are still only partially highlighted.
Jinja supports both multiple targets ({% set key, value = ... %}) and block assignments ({% set body %}...{% endset %}), but the current rules don't cover these cases. The generic keyword matcher explicitly excludes names followed by commas or equals, and the assignment matcher only marks names immediately before =. This leaves key unhighlighted in multi-target assignments and body unhighlighted in block assignments.
A set-specific target matcher that handles names followed by =, ,, or end-of-statement would correctly highlight assignment targets in all valid forms.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@syntaxes/external/jinja.tmLanguage.json` around lines 143 - 152, Add a Jinja
"set"‑specific target matcher to cover multi-target and block assignments:
create a pattern used when parsing the "set" tag that captures target names as
"variable.parameter.jinja" when they are followed by an equals sign, a comma, or
the end of the statement (e.g. match names with lookahead for
"\\s*(?:=|,|\\s*%\\})"), and mark the "=" with
"keyword.operator.assignment.jinja" (replace or supplement the existing generic
assignment pattern "\\b([a-zA-Z_][a-zA-Z0-9_]*)\\s*(=)(?!=)" and its captures)
so that both "{% set key, value = %}" and "{% set body %}...{% endset %}"
targets are highlighted.
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! 🚀 New features to boost your workflow:
|
is nottest syntax, variable assignment, and function callsuntilto conditional keywords, clean up scope namesScreenshots:

Before:
After:
